﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Opennav.MapGenerator.Locator;
using Opennav.MapGenerator.OSM;
using Opennav.MapGenerator.Geometry;
using Opennav.MapGenerator.Map;
using Opennav.MapGenerator.Map.MapTiles;
using Opennav.MapGenerator.Map.Roads;
using System.Text.RegularExpressions;
using Opennav.MapGenerator.Map.Labels;
using System.Diagnostics;
using Opennav.MapGenerator.Map.Locations;

namespace Opennav.MapGenerator.Map
{
    public class MapBuilder
    {
        private BinCache osm;
        private MapFile map;
        private LocatorEngine locator;
        private HashSet<uint> roadJunctions;

        public MapBuilder()
        {
            FakeBorder = null;
        }

        public string FakeBorder { get; set; }
        private bool fakeBorderFound = false;

        public void Build(MapFile map, BinCache osm)
        {
            this.osm = osm;
            this.map = map;
            CreateLocator();
            if (FakeBorder != null && !fakeBorderFound)
            {
                Console.WriteLine(string.Format("WARNING: Fake border '{0}' not found.", FakeBorder));
            }
            FindJunctions();
            CreateMapElements();
        }

        private void FindJunctions()
        {
            roadJunctions = new HashSet<uint>();
            HashSet<uint> nodes = new HashSet<uint>();
            foreach (OSMWay way in osm.Ways)
            {
                if (way.ContainsTagKey("highway"))
                {
                    foreach (uint nodeId in way.Nodes)
                    {
                        if (nodes.Contains(nodeId))
                            roadJunctions.Add(nodeId);
                        else
                            nodes.Add(nodeId);
                    }
                }
            }
        }

        private void CreateLocator()
        {
            locator = new LocatorEngine();
            foreach (OSMRelation relation in osm.Relations)
            {
                if (relation.GetTagValue("boundary") == "administrative" && relation.ContainsTagKey("admin_level"))
                {
                    int adminLevel = Convert.ToInt32(relation.GetTagValue("admin_level"));
                    string name = relation.GetTagValue("name");
                    IList<IList<Coordinate>> inners = new List<IList<Coordinate>>();
                    IList<IList<Coordinate>> outers = new List<IList<Coordinate>>();
                    foreach (OSMMember member in relation.Members)
                    {
                        if (member.Type == OSMEntityType.Way)
                        {
                            OSMWay way = osm.GetWay(member.Ref);
                            IList<Coordinate> wayCoords = osm.ResolveCoordinates(way);
                            if (wayCoords == null) goto Invalid;
                            if (member.Role == "outer" || member.Role == "exclave" || member.Role == "")
                                outers.Add(wayCoords);
                            else if (member.Role == "inner" || member.Role == "enclave")
                                inners.Add(wayCoords);
                        }
                    }
                    Polygon[] outerPolygons = Polygon.ParseFromWays(outers).ToArray();
                    if (outerPolygons.Length == 0) goto Invalid;
                    Polygon[] innerPolygons = Polygon.ParseFromWays(inners).ToArray();
                    locator.AddArea(name, adminLevel * 2, new Multipolygon(outerPolygons, innerPolygons));
                    //if (name == "Saarland")
                    //if (FakeBorder != null && name == "Leipzig, Stadt")
                    //if (name == "Landkreis München")
                    if (FakeBorder != null && name == FakeBorder)
                    {
                        locator.AddArea("Deutschland", 4, new Multipolygon(outerPolygons, innerPolygons));
                        adminLevel = 2;
                        fakeBorderFound = true;
                    }

                    if (adminLevel == 2)
                    {
                        foreach (Polygon p in outerPolygons)
                        {
                            map.AddMapObject(new MapLine(MapLineType.BorderCountry, p.Coordinates, 12, 24));
                        }
                        foreach (Polygon p in innerPolygons)
                        {
                            map.AddMapObject(new MapLine(MapLineType.BorderCountry, p.Coordinates, 12, 24));
                        }
                    }
                }
            Invalid:
                continue;
            }
            List<Tuple<MapPointType, GeoPlace>> placeMap = new List<Tuple<MapPointType, GeoPlace>>();
            foreach (OSMNode node in osm.Nodes)
            {
                string placeType;
                if ((placeType = node.GetTagValue("place")) != null)
                {
                    string name = node.GetTagValue("name");
                    if (name == null) continue;
                    string isin = node.GetTagValue("is_in");
                    switch (placeType)
                    {
                        case "city": placeMap.Add(new Tuple<MapPointType, GeoPlace>(MapPointType.City1, locator.AddPlace(name, isin, 16, node.Coordinate))); break;
                        case "town": placeMap.Add(new Tuple<MapPointType, GeoPlace>(MapPointType.City2, locator.AddPlace(name, isin, 18, node.Coordinate))); break;
                        case "village": placeMap.Add(new Tuple<MapPointType, GeoPlace>(MapPointType.City3, locator.AddPlace(name, isin, 20, node.Coordinate))); break;
                        case "hamlet": placeMap.Add(new Tuple<MapPointType, GeoPlace>(MapPointType.City4, locator.AddPlace(name, isin, 22, node.Coordinate))); break;
                        case "suburb": placeMap.Add(new Tuple<MapPointType, GeoPlace>(MapPointType.Suburb, locator.AddPlace(name, isin, 22, node.Coordinate))); break;
                        default: continue;
                    }
                }
            }
            locator.Process();
            foreach (Tuple<MapPointType, GeoPlace> p in placeMap)
            {
                GeoPlace place = p.Item2;
                MapPointType type = p.Item1;
                if (place != null)
                {
                    City city = map.CreateCity(place);
                    if (city == null) continue;
                    switch (type)
                    {
                        case MapPointType.City1:
                            {
                                MapPoint point = new MapPoint(type, place.Coordinate, city, 14, 24);
                                city.MapPoint = point;
                                map.AddMapObject(point);
                            }
                            break;
                        case MapPointType.City2:
                            {
                                MapPoint point = new MapPoint(type, place.Coordinate, city, 18, 24);
                                city.MapPoint = point;
                                map.AddMapObject(point);
                            }
                            break;
                        case MapPointType.City3:
                            {
                                MapPoint point = new MapPoint(type, place.Coordinate, city, 22, 24);
                                city.MapPoint = point;
                                map.AddMapObject(point);
                            }
                            break;
                        case MapPointType.City4:
                        case MapPointType.Suburb:
                            {
                                MapPoint point = new MapPoint(type, place.Coordinate, city, 22, 24);
                                city.MapPoint = point;
                                map.AddMapObject(point);
                            }
                            break;
                        default: break;
                    }
                }
            }
        }

        private RoadJunction CreateJunction(OSMNode node)
        {
            RoadJunction junction = new RoadJunction(node.Coordinate);
            if (node.GetTagValue("highway") == "mini_roundabout")
                junction.IsRoundabout = true;
            if (node.GetTagValue("highway") == "turning_cycle")
                junction.IsTurningCircle = true;
            return junction;
        }

        private void CreateMapElements()
        {
            Dictionary<uint, RoadJunction> nodeJunctions = new Dictionary<uint, RoadJunction>();
            Regex maxspeedParser = new Regex("^(?<speed>[0-9][\\.0-9]+?)(?:[ ]?(?<unit>(?:kmh|km/h|mph|kph)))?$");
            foreach (var fullway in osm.Ways)
            {
                string highwayType;
                if ((highwayType = fullway.GetTagValue("highway")) != null)
                {
                    int minRes, maxRes = 24;
                    RoadType roadType;
                    MapLineType lineType;
                    highwayType = highwayType.ToLower().Trim();
                    switch (highwayType)
                    {
                        case "motorway":
                        case "motorway_link":
                            roadType = RoadType.Motorway;
                            lineType = MapLineType.Motorway;
                            minRes = 20;
                            break;
                        case "trunk":
                        case "trunk_link":
                        case "primary":
                        case "primary_link":
                            roadType = RoadType.Class2;
                            lineType = MapLineType.Type1;
                            minRes = 21;
                            break;
                        case "secondary":
                        case "secondary_link":
                            roadType = RoadType.Type2;
                            lineType = MapLineType.Type2;
                            minRes = 22;
                            break;
                        case "tertiary":
                        case "tertiary_link":
                            roadType = RoadType.Type3;
                            lineType = MapLineType.Type3;
                            minRes = 23;
                            break;
                        case "unclassified":
                            roadType = RoadType.Type4;
                            lineType = MapLineType.Type4;
                            minRes = 23;
                            break;
                        case "residential":
                        case "living_street":
                        case "minor":
                            roadType = RoadType.Type5;
                            lineType = MapLineType.Type5;
                            minRes = 24;
                            break;
                        case "track":
                        case "unsurfaced":
                            roadType = RoadType.Track;
                            lineType = MapLineType.Track;
                            minRes = 24;
                            break;
                        default:
                            continue;
                    }
                    List<Label> labels = new List<Label>();
                    string name = fullway.GetTagValue("name");
                    string refr = fullway.GetTagValue("ref");
                    string int_refr = fullway.GetTagValue("int_ref");
                    if (name != null)
                    {
                        string[] splittedNames = name.Split(';');
                        foreach (string splittedName in splittedNames)
                            labels.Add(map.CreateLabel(splittedName.Trim()));
                    }
                    if (refr != null)
                    {
                        string[] splittedRefrs = refr.Split(';');
                        foreach (string splittedRefr in splittedRefrs)
                            labels.Add(map.CreateLabel(splittedRefr.Trim()));
                    }
                    if (int_refr != null)
                    {
                        string[] splittedRefrs = int_refr.Split(';');
                        foreach (string splittedRefr in splittedRefrs)
                            labels.Add(map.CreateLabel(splittedRefr.Trim()));
                    }
                    byte maxspeed = 0;
                    string ms = fullway.GetTagValue("maxspeed");
                    if (ms != null)
                    {
                        Match m = maxspeedParser.Match(ms);
                        if (m.Success)
                        {
                            float speed = float.Parse(m.Groups["speed"].Value.Replace(".", ","));
                            string unit = m.Groups["unit"].Value;
                            int s = 0;
                            if (unit == "mph")
                                s = (int)(speed * 1.609344f);
                            else
                                s = (int)speed;
                            if (s > 255) s = 0;
                            maxspeed = (byte)s;
                        }
                    }
                    bool toll = false;
                    switch (fullway.GetTagValue("toll"))
                    {
                        case "yes":
                        case "true":
                            toll = true;
                            break;
                    }
                    bool oneway = false;
                    bool backward = false;
                    switch (fullway.GetTagValue("oneway"))
                    {
                        case "yes":
                        case "1":
                        case "true":
                            oneway = true;
                            break;
                        case "-1":
                            oneway = true;
                            backward = true;
                            break;
                    }
                    IList<OSMNode> nodes = osm.ResolveNodes(fullway);
                    if (nodes == null)
                        continue;
                    List<List<OSMNode>> splittedWays = new List<List<OSMNode>>();
                    List<OSMNode> currentSegment = new List<OSMNode>();
                    foreach (OSMNode node in nodes)
                    {
                        currentSegment.Add(node);
                        if (currentSegment.Count > 1 && (roadJunctions.Contains(node.Id) || node == nodes.Last()))
                        {
                            splittedWays.Add(currentSegment);
                            currentSegment = new List<OSMNode>();
                            currentSegment.Add(node);
                        }
                    }
                    Map.Roads.Road road = null;
                    foreach (List<OSMNode> waySegment in splittedWays)
                    {
                        double length = 0;
                        for (int i = 1; i < waySegment.Count; i++)
                        {
                            length += waySegment[i].Coordinate.Distance(waySegment[i - 1].Coordinate);
                        }
                        Debug.Assert(length <= ushort.MaxValue);
                        Coordinate probe = waySegment[waySegment.Count / 2].Coordinate;
                        City city = map.CreateCity(locator.FindPlace(probe));
                        if (road == null || road.City != city)
                        {
                            road = new Road(labels, city);
                            map.AddRoad(road);
                        }
                        RoadJunction junctionFrom;
                        if (!nodeJunctions.TryGetValue(waySegment[0].Id, out junctionFrom))
                        {
                            junctionFrom = CreateJunction(waySegment[0]);
                            nodeJunctions[waySegment[0].Id] = junctionFrom;
                        }
                        RoadJunction junctionTo;
                        if (!nodeJunctions.TryGetValue(waySegment[waySegment.Count - 1].Id, out junctionTo))
                        {
                            junctionTo = CreateJunction(waySegment[0]);
                            nodeJunctions[waySegment[waySegment.Count - 1].Id] = junctionTo;
                        }
                        RoadSegment segment = new RoadSegment()
                        {
                            Backward = backward,
                            From = junctionFrom,
                            To = junctionTo,
                            Length = (ushort)length,
                            MaxSpeed = (byte)maxspeed,
                            OneWay = oneway,
                            Road = road,
                            Toll = toll,
                            Type = roadType
                        };
                        road.Segments.Add(segment);
                        junctionFrom.Segments.Add(segment);
                        junctionTo.Segments.Add(segment);
                        List<Coordinate> coordinates = new List<Coordinate>();
                        foreach (OSMNode node in waySegment)
                            coordinates.Add(node.Coordinate);
                        MapLine mapLine = new MapLine(lineType, coordinates.ToArray(), segment, minRes, maxRes);
                        segment.MapLines.Add(mapLine);
                    }
                }

                string railwayType = fullway.GetTagValue("railway");
                if (railwayType != null)
                {
                    int minRes, maxRes = 24;
                    MapLineType? lineType = null;
                    switch (railwayType)
                    {
                        case "narrow_gauge":
                        case "rail": lineType = MapLineType.Rail; minRes = 23; break;
                        case "light_rail":
                        case "tram": lineType = MapLineType.Tram; minRes = 24; break;
                        case "subway": lineType = MapLineType.Subway; minRes = 23; break;
                        case "monorail": lineType = MapLineType.Monorail; minRes = 23; break;
                        case "funicular": lineType = MapLineType.Funicular; minRes = 23; break;
                        default:continue;
                    }
                    IList<Coordinate> coordinates = osm.ResolveCoordinates(fullway);
                    if (coordinates == null || coordinates.Count < 2) continue;
                    map.AddMapObject(new MapLine((MapLineType)lineType, coordinates, minRes, maxRes));
                }

                string naturalType = fullway.GetTagValue("natural");
                if (naturalType != null)
                {
                    int minRes, maxRes = 24;
                    MapShapeType? shapeType = null;
                    MapLineType? lineType = null;
                    switch (naturalType)
                    {
                        case "water": shapeType = MapShapeType.Lake; minRes = 21; break;
                        case "marsh":
                        case "wetland": shapeType = MapShapeType.Wetland; minRes = 21; break;
                        case "beach": shapeType = MapShapeType.Beach; minRes = 21; break;
                        case "glacier": shapeType = MapShapeType.Glacier; minRes = 21; break;
                        case "coastline": lineType = MapLineType.Coast; minRes = 12; break;
                        case "wood": shapeType = MapShapeType.Forest; minRes = 21; break;
                        case "cave_entrance":
                        case "cliff":
                        case "fell":
                        case "heath":
                        case "land":
                        case "mud":
                        case "peak":
                        case "sand":
                        case "scree":
                        case "scrub":   // brauchen wir!
                        case "spring":
                        default: continue;
                    }
                    string name = fullway.GetTagValue("name");
                    Label label = (name != null ? map.CreateLabel(name) : null);
                    IList<Coordinate> coordinates = osm.ResolveCoordinates(fullway);
                    if (coordinates == null || coordinates.Count < 2) continue;
                    if (shapeType != null)
                    {
                        if (fullway.Nodes.First() != fullway.Nodes.Last())
                        {
                            List<Coordinate> shape = new List<Coordinate>();
                            for (int i = 0; i < coordinates.Count; i++)
                                shape.Add(coordinates[i]);
                            for (int i = 1; i < coordinates.Count; i++)
                                shape.Add(coordinates[(coordinates.Count - i) - 1]);
                            coordinates = shape;
                        }
                        map.AddMapObject(new MapShape((MapShapeType)shapeType, coordinates.ToArray(), label, minRes, maxRes));
                    }
                    else
                        map.AddMapObject(new MapLine((MapLineType)lineType, coordinates.ToArray(), label, minRes, maxRes));
                }

                string waterwayType = fullway.GetTagValue("waterway");
                if (waterwayType != null)
                {
                    int minRes, maxRes = 24;
                    MapLineType? lineType = null;
                    MapShapeType? shapeType = null;
                    switch (waterwayType)
                    {
                        case "river": lineType = MapLineType.River; minRes = 22; break;
                        case "canal": lineType = MapLineType.River; minRes = 23; break;
                        case "stream": 
                        case "ditch":
                        case "drain": lineType = MapLineType.Stream; minRes = 24; break;
                        case "riverbank": shapeType = MapShapeType.River; minRes = 22; break;
                        case "weir": lineType = MapLineType.Weir; minRes = 24; break;
                        case "dam": shapeType = MapShapeType.Dam; minRes = 23; break;
                        default: continue;
                    }
                    string name = fullway.GetTagValue("name");
                    Label label = (name != null ? map.CreateLabel(name) : null);
                    IList<Coordinate> coordinates = osm.ResolveCoordinates(fullway);
                    if (coordinates == null || coordinates.Count < 2) continue;
                    if (shapeType != null)
                    {
                        if (fullway.Nodes.First() != fullway.Nodes.Last())
                        {
                            List<Coordinate> shape = new List<Coordinate>();
                            for (int i = 0; i < coordinates.Count; i++)
                                shape.Add(coordinates[i]);
                            for (int i = 1; i < coordinates.Count; i++)
                                shape.Add(coordinates[(coordinates.Count - i) - 1]);
                            coordinates = shape;
                        }
                        map.AddMapObject(new MapShape((MapShapeType)shapeType, coordinates.ToArray(), label, minRes, maxRes));
                    }
                    else
                        map.AddMapObject(new MapLine((MapLineType)lineType, coordinates.ToArray(), label, minRes, maxRes));
                }

                if (fullway.ContainsTagKey("building"))
                {
                    IList<Coordinate> coordinates = osm.ResolveCoordinates(fullway);
                    if (coordinates == null || coordinates.Count < 2) continue;
                    map.AddMapObject(new MapShape(MapShapeType.Building, coordinates.ToArray(), null, 23, 24));
                }

                string landuseType = fullway.GetTagValue("landuse");
                if (landuseType != null)
                {
                    int minRes, maxRes = 24;
                    MapShapeType shapeType;
                    switch (landuseType)
                    {
                        case "commercial":
                        case "industrial": shapeType = MapShapeType.Industrial; minRes = 21; break;
                        case "military": shapeType = MapShapeType.Military; minRes = 23; break;
                        case "garages":
                        case "residential": shapeType = MapShapeType.Residential; minRes = 21; break;
                        case "basin":
                        case "reservoir": shapeType = MapShapeType.Lake; minRes = 22; break;
                        case "cemetary": shapeType = MapShapeType.Cemetary; minRes = 22; break;
                        case "retail": shapeType = MapShapeType.Shopping; minRes = 22; break;// Läden
                        case "farm":
                        case "farmland":
                        case "farmyard": shapeType = MapShapeType.Farming; minRes = 23; break;
                        case "forest": shapeType = MapShapeType.Forest; minRes = 21; break;
                        case "village_green":
                        case "grass":
                        case "meadow": shapeType = MapShapeType.Grass; minRes = 22; break;
                        case "greenhouse_horticulture": // Gewächshaus/Gartenkultur
                        case "vineyard": // Weinanbau
                        case "orchard": // Obstgarten
                        case "allotments": shapeType = MapShapeType.Cultivation; minRes = 23; break; // Kleingärten
                        case "recreation_ground": shapeType = MapShapeType.Recreation; minRes = 21; break;// Erholungsgebiet
                        case "greenfield": // Unbebaute Fläche
                        case "salt_pond": // Meerwassersaline
                        case "landfill": // Müllkippe
                        case "construction": // Baustelle
                        case "brownfield": // Beplante Bebauung
                        default: continue;
                    }
                    string name = fullway.GetTagValue("name");
                    Label label = (name != null ? map.CreateLabel(name) : null);
                    IList<Coordinate> coordinates = osm.ResolveCoordinates(fullway);
                    if (coordinates == null || coordinates.Count < 2) continue;
                    map.AddMapObject(new MapShape((MapShapeType)shapeType, coordinates.ToArray(), label, minRes, maxRes));
                }
            }
        }
    }
}
